Design Notification System

Ashish

Ashish Pratap Singh

easy

In this chapter, we will explore the low-level design of a Notification System in detail.

Let’s start by clarifying the requirements:

1. Clarifying Requirements

Before diving into the design, it’s essential to clarify how the notification system is expected to behave. Asking the right questions helps uncover assumptions, define boundaries, and shape the system with confidence and clarity.

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • The system should support sending notifications via EMAIL, SMS, and PUSH.
  • Each notification targets a single recipient and a specific channel.
  • The system should send notifications asynchronously.
  • If sending fails, the system should retry the operation a few times before giving up.
  • Notifications may contain a subject (optional) and a message body (mandatory).

1.2 Non-Functional Requirements

  • The system should follow object-oriented design with clear separation of concerns.
  • It should be extensible, allowing future support for new notification types (e.g., WhatsApp, Slack).
  • Delivery should be non-blocking, using a thread pool to manage parallel sending.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and highlighting the key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.

Let’s walk through the functional requirements and extract the relevant entities:

The system must send a message to a specific user.

This implies the need for an entity to represent the message itself, which we will call Notification. This class will encapsulate all the data related to a single message, such as its content, subject, and a unique identifier. It also requires an entity to represent the destination user, which we'll call Recipient. This class will hold the user's ID and all their potential contact points, like an email address, phone number, or push notification token.

The system must support various delivery channels, such as Email, SMS, and Push notifications.

Since the set of channels is predefined and fixed, this is a perfect use case for an enum. We will define a NotificationType enum to represent the possible channels (EMAIL, SMS, PUSH). This provides type safety and makes the code more readable.

The system should provide a simple, unified interface for clients and handle sending operations asynchronously.

To hide the complexity of factory selection, decoration, and asynchronous execution, we introduce a central facade class, the NotificationService. This class will be the main entry point for clients. It will manage a thread pool to process sending requests off the main thread, select the correct gateway, wrap it with retry logic, and execute the send operation.

These core entities define the key abstractions of the notification system and will guide the structure of our low-level design and class diagrams.

3. Designing Classes and Relationships

This section details the design of each class identified previously, including their specific attributes and methods. We will also illustrate how these classes relate to one another and highlight the key design patterns that underpin our solution.

3.1 Class Definitions

We can categorize our classes into enums, data-holding classes, and core classes that encapsulate the system's primary logic.

Enums

NotificationType

Enums

A type-safe enumeration to represent the different communication channels the system supports. It prevents errors from using invalid string literals and simplifies logic that depends on the channel type.

Data Classes

Recipient

A data container that holds all necessary contact information for a single user.

Recipient

Attributes:

  • userId: A String that uniquely identifies the user.
  • email, phoneNumber, pushToken: Optional<String> fields holding the contact details for each channel. Using Optional clearly communicates that a recipient may not have contact information for every channel.

Notification

Represents a single notification request.

Notification

Attributes:

  • id: A unique String identifier for the notification.
  • recipient: The Recipient object to whom the notification is addressed.
  • type: The NotificationType enum indicating the delivery channel.
  • message: The String content of the notification body.
  • subject: An optional String for the subject line, primarily used for emails.

Core Classes

NotificationService

Acts as the main entry point and Facade for the system. It hides the complexity of gateway creation, decoration, and asynchronous execution from the client.

NotificationService

Attributes:

  • executor: An ExecutorService (thread pool) to handle notification sending asynchronously.

Methods:

  • sendNotification(Notification notification): The primary public method. It accepts a Notification object and submits a task to the thread pool to handle the sending process.
  • shutdown(): A method to gracefully shut down the internal thread pool.

3.2 Class Relationships

Implementation

EmailGateway, SmsGateway, PushGateway, and RetryableGatewayDecorator are all implementations of the NotificationGateway interface.

Composition

  • RetryableGatewayDecorator has a NotificationGateway. It holds a reference to the gateway it decorates.
  • NotificationService has an ExecutorService to manage its thread pool.
  • A Notification has a Recipient and a NotificationType.

Dependency

  • The NotificationService uses the NotificationFactory to get a gateway and uses the RetryableGatewayDecorator to add retry logic.
  • Concrete gateways (e.g., EmailGateway) use the Notification and Recipient objects to extract the data needed for sending.

3.3 Key Design Patterns

Strategy Pattern

The NotificationGateway interface and its concrete implementations (EmailGateway, SmsGateway, etc.) embody the Strategy Pattern.

Strategy

Each gateway is a different "strategy" for sending a notification. The system can select and use the appropriate strategy at runtime based on the NotificationType.

Factory Pattern (Simple Factory)

The NotificationFactory implements a Simple Factory. It encapsulates the instantiation logic for the family of NotificationGateway objects, decoupling the NotificationService from the knowledge of which concrete gateway class to create.

Factory

Builder Pattern

The Notification.Builder inner class is used to construct Notification objects.

Builder

This pattern is ideal for objects with many parameters, especially optional ones, as it improves code readability and maintainability compared to telescoping constructors.

Decorator Pattern

The RetryableGatewayDecorator is a prime example of the Decorator Pattern. It dynamically adds behavior (retry logic) to any NotificationGateway object without affecting other objects of the same class.

Facade Pattern

The NotificationService acts as a Facade. It provides a single, simplified interface to the client, hiding the complex underlying subsystem of factories, multiple gateway types, decorators, and asynchronous processing. The client only needs to interact with sendNotification().

3.4 Full Class Diagram

NotificationSystem

4. Implementation

4.1 NotificationType Enum

Defines the types of notifications supported in the system

1class NotificationType(Enum):
2    EMAIL = "EMAIL"
3    SMS = "SMS"
4    PUSH = "PUSH"

4.2 Recipient

Represents the notification recipient.

1class Recipient:
2    def __init__(self, user_id: str, email: Optional[str] = None, phone_number: Optional[str] = None, push_token: Optional[str] = None):
3        self.user_id = user_id
4        self.email = email
5        self.phone_number = phone_number
6        self.push_token = push_token
7
8    def get_user_id(self) -> str:
9        return self.user_id
10
11    def get_email(self) -> Optional[str]:
12        return self.email
13
14    def get_phone_number(self) -> Optional[str]:
15        return self.phone_number
16
17    def get_push_token(self) -> Optional[str]:
18        return self.push_token
  • Contains user identifier and optional contact methods: email, phoneNumber, and pushToken
  • Uses Optional to represent possibly unavailable channels

4.3 Notification

Encapsulates the complete notification message to be delivered.

1class Notification:
2    def __init__(self, builder):
3        self.id = str(uuid.uuid4())
4        self.recipient = builder.recipient
5        self.type = builder.type
6        self.message = builder.message
7        self.subject = builder.subject
8
9    def get_id(self) -> str:
10        return self.id
11
12    def get_recipient(self) -> Recipient:
13        return self.recipient
14
15    def get_type(self) -> NotificationType:
16        return self.type
17
18    def get_message(self) -> str:
19        return self.message
20
21    def get_subject(self) -> str:
22        return self.subject
23
24    class Builder:
25        def __init__(self, recipient: Recipient, notification_type: NotificationType):
26            self.recipient = recipient
27            self.type = notification_type
28            self.message = None
29            self.subject = None
30
31        def message(self, message: str):
32            self.message = message
33            return self
34
35        def subject(self, subject: str):
36            self.subject = subject
37            return self
38
39        def build(self):
40            return Notification(self)
  • Includes id, recipient, type, subject, and message
  • Uses the Builder Pattern to construct flexible, optional-parameter-based notifications

4.4 NotificationGateway

Defines a contract for all notification delivery mechanisms. Each concrete implementation sends notifications through a specific channel (Email, SMS, or Push)

1class NotificationGateway(ABC):
2    @abstractmethod
3    def send(self, notification: Notification):
4        pass
5
6
7class EmailGateway(NotificationGateway):
8    def send(self, notification: Notification):
9        email = notification.get_recipient().get_email()
10        if email is None:
11            raise ValueError("Email address is required for EMAIL notification.")
12        
13        print("--- Sending EMAIL ---")
14        print(f"To: {email}")
15        print(f"Subject: {notification.get_subject()}")
16        print(f"Body: {notification.get_message()}")
17        print("---------------------\n")
18
19
20class PushGateway(NotificationGateway):
21    def send(self, notification: Notification):
22        token = notification.get_recipient().get_push_token()
23        if token is None:
24            raise ValueError("Push token is required for PUSH notification.")
25        
26        print("--- Sending PUSH Notification ---")
27        print(f"To Device Token: {token}")
28        print(f"Title: {notification.get_subject()}")
29        print(f"Body: {notification.get_message()}")
30        print("---------------------------------\n")
31
32
33class SmsGateway(NotificationGateway):
34    def send(self, notification: Notification):
35        phone = notification.get_recipient().get_phone_number()
36        if phone is None:
37            raise ValueError("Phone number is required for SMS notification.")
38        
39        print("--- Sending SMS ---")
40        print(f"To: {phone}")
41        print(f"Message: {notification.get_message()}")
42        print("-------------------\n")
  • EmailGateway: Sends notifications via email. Requires recipient's email and subject.
  • PushGateway: Sends notifications as push messages. Uses the push token.
  • SmsGateway: Sends text messages to mobile numbers. Requires a valid phone number.

4.5 NotificationFactory

Implements the Factory Pattern to instantiate appropriate gateway based on NotificationType.

1class NotificationFactory:
2    _gateway_map: Dict[NotificationType, NotificationGateway] = {}
3
4    @classmethod
5    def create_gateway(cls, notification_type: NotificationType) -> NotificationGateway:
6        if notification_type in cls._gateway_map:
7            return cls._gateway_map[notification_type]
8
9        gateway = None
10
11        if notification_type == NotificationType.EMAIL:
12            gateway = EmailGateway()
13        elif notification_type == NotificationType.SMS:
14            gateway = SmsGateway()
15        elif notification_type == NotificationType.PUSH:
16            gateway = PushGateway()
17
18        cls._gateway_map[notification_type] = gateway
19        return gateway

Uses caching (gatewayMap) to reuse gateway instances

4.6 RetryableGatewayDecorator

Implements the Decorator Pattern to enhance any NotificationGateway with retry logic.

1class RetryableGatewayDecorator(NotificationGateway):
2    def __init__(self, wrapped_gateway: NotificationGateway, max_retries: int, retry_delay_millis: int):
3        self.wrapped_gateway = wrapped_gateway
4        self.max_retries = max_retries
5        self.retry_delay_millis = retry_delay_millis
6
7    def send(self, notification: Notification):
8        attempt = 0
9        while attempt < self.max_retries:
10            try:
11                self.wrapped_gateway.send(notification)
12                return  # Success
13            except Exception as e:
14                attempt += 1
15                print(f"Error: Attempt {attempt} failed for notification {notification.get_id()}. Retrying...")
16                if attempt >= self.max_retries:
17                    print(str(e))
18                    raise Exception(f"Failed to send notification after {self.max_retries} attempts.") from e
19                time.sleep(self.retry_delay_millis / 1000.0)
  • Automatically retries failed sends up to a defined number of attempts
  • Adds delay between retries and logs retry attempts

4.7 NotificationService

The Facade and Executor-backed asynchronous orchestrator of the system.

1class NotificationService:
2    def __init__(self, pool_size: int):
3        self.executor = ThreadPoolExecutor(max_workers=pool_size)
4
5    def send_notification(self, notification: Notification):
6        def send_task():
7            gateway = RetryableGatewayDecorator(
8                NotificationFactory.create_gateway(notification.get_type()),
9                3,
10                1000
11            )
12            try:
13                gateway.send(notification)
14            except Exception as e:
15                print(f"Exception while sending notification: {e}")
16
17        self.executor.submit(send_task)
18
19    def shutdown(self):
20        self.executor.shutdown()
  • Receives notification requests and dispatches them using the appropriate decorated gateway
  • Uses a thread pool (ExecutorService) for parallel delivery
  • Wraps each send operation with retry capability

4.8 NotificationSystemDemo

Demonstrates the usage of the notification system.

1def main():
2    # 1. Setup the notification service
3    notification_service = NotificationService(10)
4
5    # 2. Define recipients
6    recipient1 = Recipient("user123", "[email protected]", None, "pushToken123")
7    recipient2 = Recipient("user456", None, "+15551234567", None)
8
9    # 3. Send various notifications using the Facade (NotificationService)
10
11    # Scenario 1: Send a welcome email
12    welcome_email = (Notification.Builder(recipient1, NotificationType.EMAIL)
13                     .subject("Welcome!")
14                     .message("Welcome to notification system")
15                     .build())
16    notification_service.send_notification(welcome_email)
17
18    # Scenario 2: Send a direct push notification
19    push_notification = (Notification.Builder(recipient1, NotificationType.PUSH)
20                        .subject("New Message")
21                        .message("You have a new message from Jane.")
22                        .build())
23    notification_service.send_notification(push_notification)
24
25    # Scenario 3: Send order confirmation SMS
26    order_sms = (Notification.Builder(recipient2, NotificationType.SMS)
27                .message("Your order for Digital Clock is confirmed")
28                .build())
29    notification_service.send_notification(order_sms)
30
31    # Wait for a moment to allow the queue processor to work
32    time.sleep(1)
33
34    # 4. Shutdown the system
35    print("\nShutting down the notification system...")
36    notification_service.shutdown()
37    print("System shut down successfully.")
38
39
40if __name__ == "__main__":
41    main()
  • Registers the service
  • Defines recipients
  • Sends sample notifications across all supported channels
  • Simulates asynchronous behavior and clean shutdown

5. Run and Test

Languages
Java
C#
Python
C++
Files11
decorator
entities
enum
factory
strategies
notification_service.py
notification_system_demo.py
main
notification_system_demo.py
Output

6. Quiz

Design Notification System - Quiz

1 / 20
Multiple Choice

Which core class is responsible for orchestrating the sending of notifications and managing asynchronous delivery in the notification system?

How helpful was this article?

Comments (3)


0/2000
Sort by
Rishabh Raj18 days ago
NotificationFactory is not as per factory pattern, it is violating ocp. i think we should use proper factory method pattern here.
The Cap17 days ago

factory can be written in multiple ways, so its not wrong

Copilot extension content script